﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace Sworm
{
    public class JoinQuery<TRoot>
    {
        private readonly MetadataBuilder metadata;
        private readonly Database db;
        private readonly SqlDialect dialect;
        private readonly JoinResultSetDescription resultSet;

        public JoinQuery(Database db, string rootAlias)
        {
            this.db = db;
            this.metadata = db.MetadataBuilder;
            this.dialect = db.Dialect;

            var rootTable = metadata.TableDataFor(typeof(TRoot));

            var rootId = new SqlIdentifier(rootAlias, dialect.EscapeAlias(rootAlias));
            this.resultSet = new JoinResultSetDescription(rootTable, rootId);
        }

        private TableData RootTable { get { return resultSet.Root.Table; } }
        private SqlIdentifier RootAlias { get { return resultSet.Root.Alias; } }

        private void ValidateNewAlias(string alias)
        {
            if (resultSet.AlreadyContainsAlias(alias))
            {
                throw new InvalidQueryException(string.Format(
                    "Alias '{0}' is already on this query", alias));
            }
        }

        private JoinDefinition RequireExistingAlias(string alias)
        {
            var item = resultSet.GetByAliasOrDefault(alias);
            if (item == null)
            {
                throw new InvalidQueryException(string.Format(
                    "Alias '{0}' does not exist on this query", alias));
            }
            return item;
        }

        public JoinQuery<TRoot> JoinLeft(string path, string alias)
        {
            Join(path, alias, JoinType.Left);
            return this;
        }

        public JoinQuery<TRoot> JoinInner(string path, string alias)
        {
            Join(path, alias, JoinType.Inner);
            return this;
        }

        private SqlIdentifier CreateAlias(string alias)
        {
            return new SqlIdentifier(alias, dialect.EscapeAlias(alias));
        }

        private void Join(string path, string aliasName, JoinType joinType)
        {
            ValidateNewAlias(aliasName);

            string[] parts = path.Split('.');
            if (parts.Length != 2)
            {
                throw new InvalidQueryException("join path must be formated 'alias.Member'");
            }
            string sourceAlias = parts[0];
            string mapName = parts[1];

            var source = RequireExistingAlias(sourceAlias);

            var mapping = source.Table.Relationships
                .SingleOrDefault(r => r.SourceMember.Name.EqualsIgnoreCase(mapName));

            if (mapping == null)
            {
                throw new InvalidQueryException(string.Format(
                    "Could not find member '{0}' on '{1}'", mapName, source.Table.ClassType));
            }

            resultSet.AddJoin(mapping, CreateAlias(aliasName), joinType);
        }

        private void Join<TSource, TDest>(string sourceAlias, Expression<Func<TSource, TDest>> path, string destAlias, JoinType joinType)
        {
            var body = path.Body as MemberExpression;
            if (body == null || body.Member == null)
            {
                throw new ArgumentException("path must be a member expression in the form x => x.Property");
            }

            Join(sourceAlias + "." + body.Member.Name, destAlias, joinType);
        }

        public JoinQuery<TRoot> JoinLeft<TSource, TDest>(string sourceAlias, Expression<Func<TSource, TDest>> path, string destAlias)
        {
            Join(sourceAlias, path, destAlias, JoinType.Left);
            return this;
        }

        public JoinQuery<TRoot> JoinInner<TSource, TDest>(string sourceAlias, Expression<Func<TSource, TDest>> path, string destAlias)
        {
            Join(sourceAlias, path, destAlias, JoinType.Inner);
            return this;
        }

        public JoinQuery<TRoot> JoinLeft<TJoin>(Expression<Func<TRoot, TJoin>> path, string alias)
        {
            Join(RootAlias.Unescaped, path, alias, JoinType.Left);
            return this;
        }

        public JoinQuery<TRoot> JoinInner<TJoin>(Expression<Func<TRoot, TJoin>> path, string alias)
        {
            Join(RootAlias.Unescaped, path, alias, JoinType.Inner);
            return this;
        }


        private StringBuilder BuildSelectFrom()
        {
            StringBuilder sql = new StringBuilder();
            sql.Append("select").AppendLine();

            bool first = true;
            foreach (var join in resultSet.JoinsIncludeRoot)
            {
                foreach (var column in join.Table.Columns) // TODO shouldn't assume we want all the columns
                {
                    sql.Append("   ");

                    if (!first)
                    {
                        sql.Append(", ");
                    }
                    first = false;

                    var columnAlias = GetColumnAlias(join.Alias.Unescaped, column.Name.Unescaped);
                    sql.AppendFormat(@"{0}.{1} as {2}",
                        join.Alias.Escaped, column.Name.Escaped, columnAlias.Escaped)
                        .AppendLine();

                    join.AddKnownColumn(column, columnAlias);
                }
            }

            sql.AppendFormat("from {0} {1} ", resultSet.Root.Table.Name.Escaped,
                resultSet.Root.Alias.Escaped).AppendLine();

            foreach (var join in resultSet.JoinsExcludeRoot)
            {
                sql.AppendFormat("{0} join {1} {2}", join.JoinType.ToString(), join.Table.Name.Escaped, join.Alias.Escaped);

                sql.AppendFormat(" on {0}.{1} = {2}.{3}",
                    join.Parent.Alias.Escaped, join.Relationship.SourceColumn.Name.Escaped,
                    join.Alias.Escaped, join.Relationship.DestinationColumn.Name.Escaped)
                    .AppendLine();
            }

            return sql;
        }

        public List<TRoot> List()
        {
            List<TRoot> retval = new List<TRoot>();

            string sql = BuildSelectFrom().Append(this.sql).ToString();

            db.ExecuteReader(sql, paramCollector.Params, reader =>
            {
                retval = new JoinMapper<TRoot>(resultSet, reader).Map();
            });

            return retval;
        }

        private SqlIdentifier GetColumnAlias(string alias, string sqlName)
        {
            return dialect.CreateColumnAlias(alias, sqlName);
        }

        private bool? include;
        public JoinQuery<TRoot> Include(params string[] columns)
        {
            if (include.HasValue && !include.Value)
            {
                throw new InvalidOperationException("cannot call Include() after Exclude()");
            }
            include = true;

            throw new NotImplementedException("TODO");
        }

        public JoinQuery<TRoot> Exclude(params string[] columns)
        {
            if (include.HasValue && include.Value)
            {
                throw new InvalidOperationException("cannot call Exclude() after Include()");
            }
            include = false;

            throw new NotImplementedException("TODO");
        }

        private StringBuilder sql = new StringBuilder();
        private ParamCollector paramCollector = new ParamCollector();

        public JoinQuery<TRoot> Append(string sql)
        {
            this.sql.AppendLine(sql);
            return this;
        }

        public JoinQuery<TRoot> Append(string sql, params object[] args)
        {
            paramCollector.Collect(ref sql, args);
            this.sql.AppendLine(sql);
            return this;
        }

        private bool firstWhere = true;
        public JoinQuery<TRoot> Where(string sql, params object[] args)
        {
            if (firstWhere)
            {
                this.sql.Append(" where (");
                firstWhere = false;
            }
            else
            {
                this.sql.Append(" and (");
            }

            Append(sql, args);
            this.sql.Append(")");
            return this;
        }

        private bool firstOrderBy = true;
        public JoinQuery<TRoot> OrderBy(string sql)
        {
            if (firstOrderBy)
            {
                this.sql.Append(" order by ");
                firstOrderBy = false;
            }
            else
            {
                this.sql.Append(", ");
            }
            return Append(sql);
        }
    }
}
